/*
 * Lenovo Group
 * Copyright (c) 2015-2016 All Rights Reserved.
 * 
 * Project Name:assign-tracer
 * Create Time: 2016年2月19日 下午11:35:01
 */
package com.lenovo.lmrp.server.assigntracer.domain.workflow;

import java.io.File;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;

import org.activiti.engine.ProcessEngine;
import org.activiti.engine.ProcessEngines;
import org.activiti.engine.RepositoryService;
import org.activiti.engine.RuntimeService;
import org.activiti.engine.impl.ProcessEngineImpl;
import org.activiti.engine.impl.bpmn.behavior.CallActivityBehavior;
import org.activiti.engine.impl.bpmn.behavior.ManualTaskActivityBehavior;
import org.activiti.engine.impl.bpmn.behavior.ScriptTaskActivityBehavior;
import org.activiti.engine.impl.context.Context;
import org.activiti.engine.impl.interceptor.Command;
import org.activiti.engine.impl.interceptor.CommandContext;
import org.activiti.engine.impl.interceptor.CommandExecutor;
import org.activiti.engine.impl.persistence.entity.ProcessDefinitionEntity;
import org.activiti.engine.impl.pvm.process.ActivityImpl;
import org.activiti.engine.repository.Deployment;
import org.activiti.engine.repository.DeploymentBuilder;
import org.activiti.engine.repository.ProcessDefinition;
import org.activiti.engine.runtime.Execution;
import org.activiti.engine.runtime.ProcessInstance;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.lenovo.lmrp.api.model.entity.ProcessActivityPO;
import com.lenovo.lmrp.api.model.entity.ProcessPO;
import com.lenovo.lmrp.api.model.entity.ProcessPO.ProcessStatus;
import com.lenovo.lmrp.api.model.entity.TicketProcessPO;
import com.lenovo.lmrp.api.model.view.ChangedTicketVO;
import com.lenovo.lmrp.base.FileSystemService;
import com.lenovo.lmrp.base.IFileService;
import com.lenovo.sdc.framework.util.AttributeBean;
import com.lenovo.sdc.framework.util.CommonUtil;
import com.lenovo.sdc.framework.util.exception.ErrorCode;
import com.lenovo.sdc.framework.util.exception.TraceableException;

/**
 * Convention for process definition: use CallActivity as work node in main process, 
 *  should add processMainFlag and processPagePath variant in main process
 * 
 * @author wujin
 * @version v1.0
 */
public class ProcessServiceImpl implements IProcessService {
    private static Logger logger = LoggerFactory.getLogger(ProcessServiceImpl.class);
    private static ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
    private static volatile Map<String, Object> lockMap = new ConcurrentHashMap<>();
    
    public ProcessServiceImpl() {
        processEngine = ProcessEngines.getDefaultProcessEngine();
    }
    
    public ProcessServiceImpl(ProcessEngine processEngine) {
        ProcessServiceImpl.processEngine = processEngine;
    }
    
    @Override //部署流程
    public ProcessPO deploayProcess(String itsmCode, String bpmnPath) {
        ProcessPO po = null;
        try {
            // read bpmn file to string buffer
            File file = new File(bpmnPath);
            IFileService fileService = new FileSystemService(new AttributeBean());
            StringBuilder buf = new StringBuilder();
            fileService.readFile(bpmnPath, buf);

            // deploy to activiti db
            RepositoryService repositoryService = processEngine.getRepositoryService();
            DeploymentBuilder deploymendBuilder = repositoryService.createDeployment().category(itsmCode)
                .addString(itsmCode + "_" + file.getName(), buf.toString());
            Deployment deploy = deploymendBuilder.deploy();

            // obtain process definition ID
            ProcessDefinition pd0 = processEngine.getRepositoryService().createProcessDefinitionQuery()
                .deploymentId(deploy.getId()).singleResult();

            // obtain process definition of all activitys
            CommandExecutor commandExecutor = ((ProcessEngineImpl) processEngine).getProcessEngineConfiguration()
                .getCommandExecutor();

            ProcessDefinitionEntity pd = commandExecutor.execute(new Command<ProcessDefinitionEntity>() {
                public ProcessDefinitionEntity execute(CommandContext commandContext) {
                    return Context.getProcessEngineConfiguration().getDeploymentManager()
                        .findDeployedProcessDefinitionById(pd0.getId());
                }
            });

            // convert to ProcessPO object
            po = new ProcessPO();
            po.setBpmnFilePath(bpmnPath);
            po.setCreateTime(new Date());
            po.setUpdateTime(new Date());
            po.setItsmCode(itsmCode);
            po.setStatus(ProcessStatus.PUBLISHED);
            po.setName(pd.getName());
            po.setProcessKey(pd.getKey());
            po.setProcessId(pd.getId());
            po.setProcessForm((String) pd.getVariables().get("processPagePath"));
            po.setMainFlag(pd.getVariables().get("processMainFlag") == null ? false : 
                (boolean) pd.getVariables().get("processMainFlag"));

            // convert process node to make activity_form po
            List<ActivityImpl> actions = pd.getActivities();
            List<ProcessActivityPO> callActions = new ArrayList<>(actions.size());
            for (int i = 0; i < actions.size(); i++) {
                // currently, we design main process by using CallActivity task, action detail in 
                // subprocess, these subprocess can be resued in other main process.
                // main process and sbuprocess can be deployed indenpendly
                if (po.getMainFlag()) {
                    if (actions.get(i).getActivityBehavior().getClass() == CallActivityBehavior.class) {
                        callActions.add(createActivityPO(po.getProcessId(), actions.get(i), i));
                    }
                }
                else {
                    if (actions.get(i).getActivityBehavior().getClass() == ScriptTaskActivityBehavior.class
                        || actions.get(i).getActivityBehavior().getClass() == CallActivityBehavior.class
                        || actions.get(i).getActivityBehavior().getClass() == ManualTaskActivityBehavior.class) {
                        callActions.add(createActivityPO(po.getProcessId(), actions.get(i), i));
                    }
                }
            }
            po.setActivitys(callActions);
        }
        catch (Exception exp) {
            throw new TraceableException(ErrorCode.FILE_PARSE_FAILED, exp);
        }

        return po;
    }

    /**
     * 
     * @param po
     * @param actions
     * @param callActions
     * @param i
     */
    private ProcessActivityPO createActivityPO(String processID, ActivityImpl action, int index) {
        ProcessActivityPO actPO = new ProcessActivityPO();
        actPO.setProcessId(processID);
        actPO.setActivityId(processID + ":" + String.format("%02d", index));
        actPO.setCanSkip(false);
        actPO.setShowForApp(false);
        actPO.setActivityKey(action.getId());
        actPO.setActivityName((String) action.getProperty("name"));
        
        // parse document field to get extenstion info
        String documention = (String) action.getProperty("documentation");
        if (CommonUtil.nonNullAndEmpty(documention)) {
            String[] exts = documention.split("\n");
            for (String ext : exts) {
                if (ext.contains("acceptaction="))
                   actPO.setAcceptAction(ext.substring("acceptaction=".length()));
                else if (ext.contains("sortforapp=")) {
                    actPO.setSortForApp(Integer.parseInt(ext.substring("sortforapp=".length())));
                    actPO.setShowForApp(true);
                }
                else if (ext.contains("formurl="))
                    actPO.setActionForm(ext.substring("formurl=".length()));
                else if (ext.contains("icourl="))
                    actPO.setIcoUrl(ext.substring("icourl=".length()));
            }
        }
        return actPO;
    }

    @Override //启动流程
    public String createProcessInstance(String processID) {
        try {
            ProcessInstance pi = processEngine.getRuntimeService().startProcessInstanceById(processID);
            logger.info("create process {} instance {}", processID, pi.getId());
            return pi.getId();
        }
        catch (Exception exp) {
            throw new TraceableException(ErrorCode.COMM_PARAM_INVALID, exp);
        }
    }

    public void startProcessInstance(String instanceID, Map<String, Object> params) {
        RuntimeService runtimeService = processEngine.getRuntimeService();
        // find out processInstance object then send signal to start process
        // we assume process is follow convention has timerCacheEvent node
        ProcessInstance pi = runtimeService.createProcessInstanceQuery().processInstanceId(instanceID).singleResult();
        
        // is process instance has end return do nothing
        if (pi.isEnded()) {
            logger.warn("process {} has end", instanceID);
            return;
        }
        
        // resume suspended process
        if (pi.isSuspended()) {
            runtimeService.activateProcessInstanceById(instanceID);
            return;
        }
        
        // obtain execution of waitStartSignal, if it is in waiting status send out signal let process move on
        Execution exe = runtimeService.createExecutionQuery().processInstanceId(instanceID).activityId("waitStartSignal").singleResult();
        if (Objects.nonNull(exe)) {
            if (exe.isEnded() || exe.isSuspended()) {
                logger.warn("execution has end or suspended");
                return;
            }
        
            runtimeService.setVariables(exe.getId(), params);
            runtimeService.signalEventReceivedAsync("startSignal", exe.getId());
        }
        else {
            List<Execution> exes = runtimeService.createExecutionQuery().processInstanceId(instanceID).activityId(pi.getActivityId()).list();
            runtimeService.setVariables(exes.get(1).getId(), params);
        }
        logger.info("start process {} instance {} ", pi.getProcessDefinitionId(), instanceID);
    }
    
    @Override
    public void fireProcessInstance(TicketProcessPO ticketProcessPO, ChangedTicketVO vo) {
        if (lockMap.containsKey(ticketProcessPO.getTicketId()))
            return;
        else {
            synchronized (lockMap) {
                if (lockMap.containsKey(ticketProcessPO.getTicketId()))
                    return;
                
                lockMap.put(ticketProcessPO.getTicketId(), new Object());
            }
        }
        
        RuntimeService runtimeService = processEngine.getRuntimeService();
        // find out processInstance object then send signal to start process
        // we assume process is follow convention has timerCacheEvent node
        ProcessInstance pi = runtimeService.createProcessInstanceQuery()
                        .processInstanceId(ticketProcessPO.getInstanceId()).singleResult();
        // is process instance has end return do nothing
        if (pi.isEnded()) {
            logger.warn("process {} has end", ticketProcessPO.getInstanceId());
            return;
        }
        
        if (pi.isSuspended())
            runtimeService.activateProcessInstanceById(ticketProcessPO.getInstanceId());
            
        logger.info("begin to trigger process {} for {} ticket {} do {}", 
            ticketProcessPO.getItsmCode(), ticketProcessPO.getTicketId(), ticketProcessPO.getInstanceId(), vo.getAction());
        
        // find out current callactivity subprocess of main process
        List<Execution> executions = null;
        ProcessInstance subinst = runtimeService.createProcessInstanceQuery()
            .superProcessInstanceId(ticketProcessPO.getInstanceId()).singleResult();
        
        if (Objects.isNull(subinst)) {
           executions = runtimeService.createExecutionQuery().processInstanceId(ticketProcessPO.getInstanceId())
                           .signalEventSubscriptionName("alertSignal").list();
                           //.activityId(pi.getActivityId()).list();
        }
        else {
            executions = runtimeService.createExecutionQuery().processInstanceId(subinst.getId())
                            .signalEventSubscriptionName("alertSignal").list();
            //.activityId(subinst.getActivityId()).list();
        }
        
        // send out signal with parameter to push workflow move on 
        if (Objects.nonNull(executions) && executions.size() > 0) {
            Execution execution = executions.get(0);
            //Execution execution = executions.get(1);
            try {
                Map<String, Object> variableMap = new HashMap<String, Object>();
                variableMap.put("changedVO", vo);
    
                //runtimeService.setVariables(execution.getId(), variableMap);
                runtimeService.signalEventReceived("alertSignal", execution.getId(), variableMap);
                logger.info("complete notify for ticket {} action {}", ticketProcessPO.getTicketId(), vo.getAction());
            }
            finally {
                lockMap.remove(ticketProcessPO.getTicketId());
            }
        }
        else {
            logger.warn("not found event execution when do {} for ticket {}", vo.getAction(), ticketProcessPO.getTicketId());
        }
    }

    @Override
    public void stopProcessInstance(String instanceID) {
        try {
            processEngine.getRuntimeService().deleteProcessInstance(instanceID, "no reason");
        }
        catch (Exception exp) {
            TraceableException.writeLog(ErrorCode.COMM_ERROR, "", exp);
        }
    }
}
